import type {
  ReactNode,
} from 'react'
import {
  createContext,
  useContext,
  useEffect,
  useRef,
} from 'react'
import { proxy } from 'valtio'
import { compile } from '../compiler'
import { decompress } from '../utils/compression'
import type { LocalStorageAgent } from './localStorageAgent'
import { createLocalStorageAgent } from './localStorageAgent'

const StoreContext = createContext<Store | null>(null)

export interface Settings {
  name: string
  code?: string
  defaultCode?: string
  compressedCode?: string
  importMap?: string
  placeHolder?: string
  enableStorage?: boolean
  enableTabs?: boolean
}

function StoreContextProvider({
  children,
  settings,
}: {
  children: ReactNode
  settings: Settings
}) {
  const storeRef = useRef<Store>()
  if (!storeRef.current)
    storeRef.current = initStore(settings)

  useEffect(() => {
    if (settings.code && storeRef.current)
      // if the code changed, notify the store to update code
      storeRef.current.updateCode(settings.code)
  }, [settings.code])

  return (
    <StoreContext.Provider value={storeRef.current}>
      {children}
    </StoreContext.Provider>
  )
}

export const INDEX_FILE_NAME = 'index.jsx'

class Store {
  public compressedCode: string = ''
  public compiledFiles: Record<string, string> = {}
  public compileError: string = ''
  constructor(
    public ls: LocalStorageAgent,
    public name: string,
    public files: Record<string, string>,
    public currentFile: string,
    public importMap?: string,
    public placeHolder?: string,
    public enableStorage: boolean = true,
    public enableTabs: boolean = true,
  ) { }

  get fileNames() {
    return this.files ? Object.keys(this.files) : []
  }

  get code() {
    return this.files?.[this.currentFile]
  }

  updateCode(code: string) {
    const temp = { ...this.files }
    temp[this.currentFile] = code
    this.files = temp
    this.ls.set(temp)
  }

  clearCompileError() {
    this.compileError = ''
  }

  // add file named `Comp${index}`
  addFile() {
    let i = 1
    while (this.files[`Comp${i}.jsx`] !== undefined)
      i++

    this.files = {
      ...this.files,
      [`Comp${i}.jsx`]: genNewCompFile(`Comp${i}`),
    }
  }

  switchFile(fileName: string) {
    this.currentFile = fileName
  }

  changeFileName({ oldName, newName }: { oldName: string, newName: string }) {
    const temp = { ...this.files }
    while (temp[newName] !== undefined) {
      // eslint-disable-next-line no-alert
      newName = prompt('File name already exists, please enter a new name')!
      if (!newName)
        return
    }
    temp[newName] = temp[oldName]
    delete temp[oldName]
    this.files = temp
    if (this.currentFile === oldName)
      this.currentFile = newName
  }

  deleteFile(fileName: string) {
    const temp = { ...this.files }
    delete temp[fileName]
    this.files = temp
  }
}

function initStore({
  name,
  code,
  defaultCode,
  importMap,
  placeHolder,
  enableStorage,
  enableTabs,
  compressedCode,
}: Settings) {
  const ls = createLocalStorageAgent(name, enableStorage)
  const initialFiles
    = (code && { [INDEX_FILE_NAME]: code } as Record<string, string>)
    || (compressedCode && decompress(compressedCode))
    || ls.get()
    || { [INDEX_FILE_NAME]: defaultCode } as Record<string, string>

  const store = new Store(ls, name, initialFiles, INDEX_FILE_NAME, importMap, placeHolder, enableStorage, enableTabs)

  if (initialFiles)
    tryCompile(store)

  const state = proxy(store)
  return state
}

export function isValidScript(fileName: string) {
  return fileName.endsWith('.jsx') || fileName.endsWith('.tsx')
}

export function tryCompile(state: Store) {
  try {
    const compiledFiles: Record<string, string> = {}
    const importGraph: Record<string, string[]> = {}
    const css: string[] = []
    Object.keys(state.files).forEach((fileName) => {
      const code = state.files[fileName]
      if (!code)
        return

      if (fileName.endsWith('.css')) {
        css.push(code)
      }
      else if (isValidScript(fileName)) {
        const compiledCode = compile(code, fileName, importGraph)
        if (compiledCode)
          compiledFiles[fileName] = compiledCode
      }
      else {
        throw new Error(`Unsupported file type: ${fileName}`)
      }
    })
    state.compileError = ''
    return [compiledFiles, importGraph, css.join('\n')] as const
  }
  catch (e) {
    if (e instanceof Error)
      state.compileError = e.message
  }
  return null
}

function useStore() {
  const store = useContext(StoreContext)
  if (!store)
    throw new Error('Store is not initialized')

  return store
}

function genNewCompFile(fileName: string) {
  return `export default function ${fileName}() {
  return <div> Your code here </div>;
}
`
}

export { StoreContextProvider, useStore }
